//Includes#include <p18cxxx.h>#include <stdlib.h>#include "define.h"#include <math.h>#include <string.h>#include <delays.h>#include "stringconst.h"//Global Variableschar recvbuf[I_BUFSIZE];//All received commands are saved here (both serial and keypress), to be processed laterchar transbuf[O_BUFSIZE];//Output to the serial port is buffered here for the serial_transmit() to sendchar dispbuf[O_BUFSIZE];//Output to the LCD screen is buffered here for the LCD to displayvolatilechar*recvbuf_iPtr =&recvbuf[0];//insertion pointervolatilechar*recvbuf_ePtr =&recvbuf[0];//removal pointervolatilechar transbufi =0;// buffer insertion index from stringvolatilechar transbufr =0;// buffer removal index to transmit shift registerunsignedint ADSamples[VBUFSIZE];//Buffer of the N most recent voltage samples (in millivolts)//massConvertLookup[i][j] is converting unit i to unit jfloat massConvertLookup[][2]={{1.0,0.0352739619},{28.349523125,1.0}};/* y = mx + b, where y is the result mass in unit i, x is the voltage,
m is the gradient voltageToMass[i][0], b is the y-intercept voltageToMass[i][1]. */float voltageToMassEquation[][2]={{60.98824,-53.51852},{1700.0,-1.0}};//The ounce y-intercept value is a bit inaccurate, but it's round here or later so I don't think it matters in the endunsignedint tareVoltage =0;//The tare, as a pure voltageunsignedint tareMass =0;char voltageH =0;//The value from ADRESHchar voltageL =0;//The value from ADRESLunsignedint sampleInterval =12500;//The increment count for the A/D sample rate. = 125 * period in ms.char newADValue = FALSE;//Is there a new ADValue?unsignedchar unit = GRAMS;//The unit currently being usedint mass =0;//The current mass, in the current unitint varMass =0;//Variance in mass;int rawMass =0;// mass for raw weight mode//Stringsunsignedchar suffixGrams[]=" grams\r\n";//the compiler treats initialised strings as constunsignedchar suffixOunces[]=" ounces\r\n";//If anyone works out a more elegant way of doing the suffixes, do sounsignedchar unitError[]=" <<UNIT ERROR>>\r\n";//An error messageunsignedchar romstring[64];//Space to dump strings when fetching from ROM (stops the memory being eaten by other bits of the program)// Keypad Lookup Tableconstchar keypad_lookuptable[]={'1','2','3','n','4','5','6','n','7','8','9','n','*','0','#'};externunsignedchar LCDWantsToSend;//Prototypesvoid low_ISR(void);void high_ISR(void);externvoid setup(void);void transmit(unsignedchar* message);void serial_receive(void);void keypad_receive(void);void store_sample(unsignedint voltage);unsignedint getVoltage(void);unsignedint getAverage(unsignedint* set,int size);unsignedint getDeviation(unsignedint* set,int size,unsignedint mean);int getMass(unsignedint voltage,char massUnit,char applyTare,char forDev);void int_to_string(int rawNumber,unsignedchar* string);void transchar(void);unsignedchar* get_rom_string(const rom unsignedchar* txPtr);float convert_unit(float convertMass,char inputUnit,char outputUnit);void calibrate(unsignedint zeroVoltage,unsignedint secondVoltage,int secondMass,char massUnit);void setTare(void);void printInstructions(char mode,char func);void generateLine1(char key,unsignedchar*function);void generateLine2(char key,unsignedchar*function);externvoid checkState (void);externunsignedchar isBusy(void);externvoid sendNext(void);#pragma config WDT = OFF //Stops the watchdog timer from restting the PIC//Interrupt Jump Vectors#pragma code high_vector = 0x0008void high_interrupt(void){
_asm goto high_ISR _endasm
}#pragma code low_vector = 0x0018void low_interrupt(void){
_asm goto low_ISR _endasm
}#pragma code/*The actual code begins here!*//**
* High priority interrupt service routine (ISR).
* Central functions triggered here are inputs by Serial Receive and Keypad.
* /
void high_ISR(void)
{
char rcdebug = 0;
static unsigned char LVDstring[] = "LOW VOLTAGE DETECTED";
/*All interrupts will check the mask (PIE) before checking the request (PIR)*///Disable ALL interrupts
INTCONbits.GIEH=0;//Check for receive intif(PIE1bits.RCIE==1&& PIR1bits.RCIF==1){
serial_receive();}//Check for external keypressif(INTCON3bits.INT1IF==1){
keypad_receive();
INTCON3bits.INT1IF=0;}if(PIE2bits.LVDIE==1&& PIR2bits.LVDIF==1){
transmit(LVDstring);}
INTCONbits.GIEH=1;}/**
* Low priority interrupt service routine (ISR).
* Central functions triggered here are Serial Transmit and receiving A/D value.
* /
void low_ISR(void)
{
/*All interrupts will check the mask (PIE)
before checking the request (PIR)*///Disable ONLY LOW interrupts
INTCONbits.GIEL=0;//Check for transmit empty intif(PIE1bits.TXIE==1&& PIR1bits.TXIF==1){
transchar();}if(PIR1bits.ADIF==1&& PIE1bits.ADIE==1)//If A/D interrupt enabled and triggered{
voltageH = ADRESH;
voltageL = ADRESL;
newADValue = TRUE;
PIR1bits.ADIF=0;}
INTCONbits.GIEL=1;}/**
* Main function loop.
* Each loop it will call the checkState() function to determine the next action to be taken by the program.
* It will also test to see if the A/D has collected a new voltage value.
* If so, it will store this value into the A/D buffer, and increment the buffer counter j.
* When j reaches the size of the buffer, the system will call getMass and getDeviation to calculate
* the mass and variance values based on the current A/D buffer.
* The main loop also sends characters to the LCD if it is ready and there is data waiting to be sent.
* /
void main(void)
{
unsigned int voltage = 0;
unsigned int aveVoltage = 0;
unsigned int varianceVolt = 0;
int varianceMass = 0;
unsigned char i = 0, j = 0;
//Clear transmit buffer at start of program
for(i = 0; i < O_BUFSIZE; i++)
transbuf[i] = 0;
setup(); //Initialise all registers
while(1)
{
if(newADValue)
{
newADValue = FALSE;
voltage = getVoltage();
store_sample(voltage);
j++;
if (j == VBUFSIZE)
{
j = 0;
aveVoltage = getAverage(ADSamples, VBUFSIZE);
mass = getMass(aveVoltage, unit, TRUE, FALSE); //apply tare
varianceVolt = getDeviation(ADSamples, VBUFSIZE, aveVoltage);
varMass = getMass(varianceVolt, unit, FALSE, TRUE);
}
}
checkState();
if(!isBusy() && LCDWantsToSend)
{
sendNext();
}
}
}
/**
* Gets the average of an array of values of pre-defined size.
* The function adds all the values in the array then divides by the size.
* @param set The array of unsigned integers to average. It need not be null-terminated in any way.
* @param size The number of integers in the array.
* @return mean Returns the mean value of the array as an unsigned integer.
*/unsignedint getAverage(unsignedint* set,int size){int i =0;longint total =0;int mean =0;for(i =0; i < size; i++){
total += set[i];}
mean = total/size;return mean;}/**
* Gets the variance of an array of values of pre-defined size, with a pre-calculated mean.
* The function used is a simple finite-population variance equation:
* variance = sqrt( 1/size * sum of (xi - mean)^2 ) for each sample xi
* @param set The array of unsigned integers to calculate variance from. It need not be null-terminated in any way.
* @param size The number of integers in the array.
* @param mean The average value of the numbers in the array.
* @return variance Returns the variance of the mumbers in the array as an unsigned integer.
*/unsignedint getVariance(unsignedint* set,int size,unsignedint mean){// variance = sqrt( 1/size * sum of (xi - mean)^2 )int i =0;float totalf =0.0, incf =0.0, sizef =0.0, meanf =0.0, valf =0.0, subf =0.0, subf2 =0.0;unsignedint variance =0;
meanf =(float)mean;
sizef =(float)size;for(i =0; i < size; i++){
valf =(float)set[i];
subf = valf - meanf;
subf2 = subf * subf;
incf = subf2/sizef;if(incf <9999.0)// Exclude values that are clearly incorrect
totalf += incf;}
variance =(unsignedint)totalf;return variance;}/**
* Stores values from the A/D converter in a small circular buffer (VINSERT = 8 values long)
* No processing is done. It is up to the getMean() and getVariance() functions to interperet the data
* Values are stored in the global array "ADSamples[]".
* @param voltage The value to be stored. The oldest values will be dropped to make way for new ones.
* @return void.
*/void store_sample(unsignedint voltage){staticchar vinsert =0;
ADSamples[vinsert]= voltage;/*advance the position in the buffer, loop around at the end*/
vinsert++;if(vinsert >= VBUFSIZE){
vinsert =0;}}/**
* A function to convert a signed integer to a string of ascii numerical characters.
* By progressively dividing the integer passed by 10s, 100s, 1000s etc, the value of
* each digit in the number can be determined. This is then used as an index to look
* up the value of the appropriate character. Each character is placed into the output
* string in the correct location so that it represents a decimal number.
* This function also checks for negative values, and affixes a '-' sign if appropriate
* @param rawNumber A signed integer less than +- ten thousand..
* @param string The return string, which is modified by this function. It will be overwritten and null terminated.
* @return void.
*/void int_to_string(int rawNumber,unsignedchar* string){char digits[11]="0123456789";//the 11th character is /0unsignedint thousands =0, hundreds =0, tens =0, ones =0;int t1 =0, t2 =0, t3 =0;int number;if(rawNumber <0){
number =0- rawNumber;
string[0]='-';}else{
number =(int)rawNumber;
string[0]=' ';}
thousands = number /1000;
hundreds =(number -1000*thousands)/100;
tens =(number -1000*thousands -100*hundreds)/10;
ones =(number -1000*thousands -100*hundreds -10*tens);
string[1]= digits[thousands];
string[2]= digits[hundreds];
string[3]= digits[tens];
string[4]= digits[ones];if(thousands ==0){
string[1]=' ';if(hundreds ==0){
string[2]=' ';if(tens ==0)
string[3]=' ';}}
string[5]='\0';}/**
* Called when the serial receive interrupt is triggered, this will read the RCREG
* and then copy the character onto the input stream (recvbuf). This function will
* modify the insertion and extraction pointers recvbuf_iPtr and recvbuf_ePtr.
* @post The character from RCREG will be copied to recvbuf, and RCREG is cleared
*/void serial_receive(void){
PIR1bits.RCIF=0;// Clear RC flagif(RCREG ==0x0D| RCREG ==0x0A){// Place special handling for 'enter' here.}*recvbuf_iPtr = RCREG;// Place recieved char into insertion location
recvbuf_iPtr = recvbuf_iPtr++;// Increment insertion pointer.if(recvbuf_iPtr > recvbuf + I_BUFSIZE){
recvbuf_iPtr =&recvbuf[0];}}/**
* Converts the input receive in PORTB from the MM74C922 encoder chip
* into a decimal number by right shifting and bit masking
* Uses #define LOW_NIBBLE for bit masking
* Uses the const char keypad_lookup table to receieve the value of
* which button entered by the user
* Usage example:
* User presses the '1' key = b00000000( in binary) = 0( in decimal)= '1' in lookup table
* @param temp Temporary value used to receieve PORTB values
* @param key_pressed A string value representing the button pressed
* @param recvbuf Used to store the string value
* @return void
*/void keypad_receive(void){char temp;char key_pressed;
temp =(PORTB>>2)&LOW_NIBBLE;// 2x Right shifting and bit masking
key_pressed = keypad_lookuptable[temp]// Getting string valur from table*recvbuf_iPtr = key_pressed;
recvbuf_iPtr = recvbuf_iPtr++;// Increment insertion pointer.if(recvbuf_iPtr > recvbuf + I_BUFSIZE){
recvbuf_iPtr =&recvbuf[0];}}/**
* Some strings are stored in the ROM. This function
* will fetch the string from ROM, place it in RAM,
* and return a pointer to the start of the RAM string.
* @param txPtr This is a pointer to the address of the start of the string in ROM. Do not pass RAM strings to this!
* @return romstring romstring is a block of (RAM) memory set aside to dump ROM strings into when they are being fetched.
*/unsignedchar* get_rom_string(const rom unsignedchar* txPtr){
strcpypgm2ram(romstring,(far rom unsignedchar*)txPtr);return romstring;}/**
* Copies a string into the transmit buffer, for later sending.
* @param trans_string The null-terminated string of ASCII characters for transmission.
*/void transmit(unsignedchar* trans_string)// Transmit function, writes string into buffer and starts sending{char i =0;
Delay1KTCYx(10);while(trans_string[i]!='\0')// until null termination of string reached{
transbuf[transbufi]= trans_string[i];// copy string into transmit buffer
transbufi++;if(transbufi >= O_BUFSIZE)// catch insertion index overflow{
transbufi =0;}
i++;}
PIE1bits.TXIE=1;// enable transmit interrupt//Because the transmit interrupt is enabled, the output string will start being sent immediately}/**
* Copies a character into the transmit shift register for transmission.
* There are no inputs as it accesses the global transmit buffer.
*/void transchar(void)// Transmit function, sends next char to TXREG as long as transmission buffer has more material to write{
TXREG = transbuf[transbufr];// load character into transmit shift register
transbufr++;// increment removal indexif(transbufr >= O_BUFSIZE){
transbufr =0;// catch removal index overflow}if(transbufr == transbufi)// if removal index catches insertion index, end of transmission{
PIE1bits.TXIE=0;// disable transmit interrupt}}/**
* Clears the HyperTerminal screen by transmitting the screen feed control character.
*/void clearScreen(void){
transmit(get_rom_string(_cls));}/**
* This is a very simple function that will make sure only allowable
* characters get passed to the state machine. Only the chars representing:
* WEIGH, COUNT, TARE, CALIBRATE, STATS, RAW and SAMPLES are valid.
* @param iChar A character which is to be checked for validity.
* @return oChar Valid characters are passed through. Invalid characters return 0.
unsigned char parseFunc(unsigned char iChar)
{
if(iChar == WEIGH|iChar == COUNT|iChar == TARE|iChar == CALIBRATE|iChar == STATS|iChar == RAW|iChar == SAMPLES)
{
return iChar;
}
else
{
return 0;
}
}
/**
* Similar to parseFunc, but for the active mode. See parseFunc documentation also.
* Ensures only characters valid for LOCAL, REMOTE or FACTORY modes are passed to the state machine.
* @param iChar A character which is to be checked for validity.
* @return oChar Valid characters are passed through. Invalid characters return 0.
*/unsignedchar parseMode(unsignedchar iChar){if(iChar == LOCAL|iChar == REMOTE|iChar == FACTORY){return iChar;}else{return0;}}/**
* Prints the user menu to HyperTerminal using serial transmit.
* The menu changes depending on the active mode (USER vs. FACTORY) and function, since different options become available.
* @param mode Either LOCAL or REMOTE, which will indicate where the instructions should be streamed to
* @param func The function for which the instructions should be displayed.
*/void printInstructions(char mode,char func){unsignedchar asterisks[]="*****";
transmit(get_rom_string(_outro));
transmit(asterisks);
transmit(get_rom_string(_outro));
transmit(get_rom_string(_intro));switch(mode){case LOCAL:
transmit(get_rom_string(_ul));break;case REMOTE:
transmit(get_rom_string(_ur));break;case FACTORY:
transmit(get_rom_string(_fy));break;}
transmit(get_rom_string(_cs));switch(func){case WEIGH:
transmit(get_rom_string(_wh));break;case COUNT:
transmit(get_rom_string(_ct));break;case STATS:
transmit(get_rom_string(_ss));break;case RAW:
transmit(get_rom_string(_rw));break;case SAMPLES:
transmit(get_rom_string(_sr));;break;case CALIBRATE:
transmit(get_rom_string(_ce));break;}
transmit(get_rom_string(_fs));
transmit(get_rom_string(_outro));switch(mode){case LOCAL:
generateLine1(REMOTE,_ur);break;case REMOTE:
generateLine1(LOCAL,_ul);break;}if(func != WEIGH){
generateLine1(WEIGH,_wh);}if(func != COUNT){
generateLine1(COUNT,_ct);}
transmit(get_rom_string(_outro));
generateLine2(TARE,_te);
generateLine2(CHANGEUNIT,_ts);if(mode == FACTORY){
transmit(get_rom_string(_outro));
transmit(get_rom_string(_fac1));
generateLine1(STATS,_ss);
generateLine1(RAW,_rw);
generateLine1(SAMPLES,_sr);
generateLine1(CALIBRATE,_ce);
transmit(get_rom_string(_outro));}
transmit(asterisks);
transmit(get_rom_string(_outro));}/**
* Generates a menu instruction line for changing the active function. The line is transmitted via serial.
* @param key A character corresponding to the key to be pressed to enter the desired function.
* @param function A string with the name of the function referred to, such as "Count" or "Weigh".
* Constant text elements "Press __ to enter __ mode." are copied from program memory with get_rom_string.
* Usage example: generateLine1('w', "Weigh") will return "Press w to enter Weigh mode."
*/void generateLine1(char key,unsignedchar*function){unsignedchar keystr[2];// assemble null-terminated string from character passed to function
keystr[0]= key;
keystr[1]='\0';
transmit(get_rom_string(_11));
transmit(keystr);
transmit(get_rom_string(_12));
transmit(get_rom_string(_13));
transmit(get_rom_string(function));
transmit(get_rom_string(_14));
transmit(get_rom_string(_outro));}/**
* Generates a menu instruction line for performing supplementary tasks (tare, change unit type). The line is transmitted via serial.
* @param key A character corresponding to the key to be pressed to perform the task.
* @param function A string with the name of the task referred to, such as "Toggle Units" or "Tare".
* Constant text elements "Press __ to __." are copied from program memory with get_rom_string.
* Usage example: generateLine2('u', "Toggle Units") will return "Press u to Toggle Units."
*/void generateLine2(char key,unsignedchar*function){unsignedchar keystr[2];// assemble null-terminated string from character passed to function
keystr[0]= key;
keystr[1]='\0';
transmit(get_rom_string(_11));
transmit(keystr);
transmit(get_rom_string(_12));
transmit(get_rom_string(function));
transmit(get_rom_string(_fs));
transmit(get_rom_string(_outro));}/**
* Returns an unsigned 16 bit integer, containing 10 bits of A/D result. The 6 most
* significant bits will be 0.
* Precondition: The A/D Converter is on and initialised.
* Precondition: The special event trigger has enabled the A/D converter, and then an interrupt
* has moved the ADRESH and ADRESL values to voltageH and voltageL, respectively.
* @return 10 bit A/D result
*/unsignedint getVoltage(void){unsignedint result =0;unsignedint lhold =0;
result = voltageH *256;
lhold = voltageL &0x00FF;
result = result + lhold;return result;//Result is 10 bits}/**
* Gets the current mass, as ounces or grams, as specified by the parameters.
* It uses either the predefined y-intercept and gradient, or one set by using
* the calibrate function.
* @param voltage The voltage corresponding to the desired mass.
* @param massUnit The unit of mass to use, either GRAMS or OUNCES (#defined above).
* @param applyTare A boolean value. A value evaluating to true will mean that the mass is adjusted for tare.
* @param forVar A boolean value. A value evaluating to true will mean that no offset is applied, since this is a variance calculation.
*/int getMass(unsignedint voltage,char massUnit,char applyTare,char forVar){//y = mx + b to get base resultfloat mx =0.0;float b =0.0;int resultmass =0;
mx = voltage *64.0/ voltageToMassEquation[massUnit][0];if(forDev == TRUE)// this is a standard deviation conversion and no offset is required
b =0.0;elseif(forDev == FALSE)
b = voltageToMassEquation[massUnit][1];//mass = voltageToMassEquation[massUnit][0] * voltage + voltageToMassEquation[massUnit][1];
resultmass =(int)(mx +0.5)+(int)(b +0.5);// rawMass = resultmass;if(applyTare)
resultmass = resultmass - tareMass;return resultmass;}/**
* Sets the tare value.
* Sets the global value tareMass to the current value of global mass.
* This is subtracted within the getMass function if a tared value is requested by the user.
*/void setTare(void){
tareMass += mass;// May give error if unit type switched after performing tare, but can be fixed by taring again.}/**
* Converts a mass from one unit to another.
* Uses the #define conventions of masses, that is, GRAMS = 0,
* and OUNCES = 1.
* Usage example:
* ounceValue = convertUnit(gramValue, GRAMS, OUNCES);
* @param convertMass The input mass
* @param inputUnit A numerical char representing the input mass type
* @param outputUnit A numerical char represeting the output mass type
* @return float The converted mass.
*/float convert_unit(float convertMass,char inputUnit,char outputUnit){
convertMass = convertMass * massConvertLookup[inputUnit][outputUnit];return convertMass;}/**
* Use two points, and simple maths, to work out a calibration line.
* If the input values are invalid, for any reason, the function will end.
* @param zeroVoltage The voltage at a zero mass point
* @param secondVoltage The voltage at some positive non-zero mass point.
* @param secondMass The positive non-zero mass point. The value is in either grams or ounces
* @param massUnit Specifies what unit secondMass is in, either GRAMS or OUNCES.
*/void calibrate(unsignedint zeroVoltage,unsignedint secondVoltage,int secondMass,char massUnit){//Algorithm://m = v * 64 / x - y//For v1, 0 and v2, m://y = v1 * 64 / x//y = v2 * 64 / x - m//Thus://v1 * 64 / x = v2 * 64 / x - m//Therefore://x = 64 * (v2 - v1) / m//y = - v1 * m / (v2 - v1)//Also://x(otherUnit) = convert_unit(x, otherUnit, massUnit); //Note order. Equivalent to division//y(otherUnit) = convert_unit(y, massUnit, otherUnit);float x =0.0;float y =0.0;char otherMassUnit =1- massUnit;//0 or 1float voltageDiff = secondVoltage - zeroVoltage;if(voltageDiff ==0)//Error handlingreturn;
x =64* voltageDiff / secondMass;
y = zeroVoltage * secondMass / voltageDiff;
y =- y;
voltageToMassEquation[massUnit][0]= x;
voltageToMassEquation[massUnit][1]= y;
voltageToMassEquation[otherMassUnit][0]= convert_unit(x, otherMassUnit, massUnit);
voltageToMassEquation[otherMassUnit][1]= convert_unit(y, massUnit, otherMassUnit);}